package com.liuqianfei.markme.rules;

import com.liuqianfei.markme.MarkMeParser;
import com.liuqianfei.markme.common.Utils;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.util.StringUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * Parser state
 * @author liuqianfei
 */
public class StateBlock extends AbstractState
{
    public String src;
    
    //****************************************************
    // link to parser instance
    //****************************************************
    public MarkMeParser md;
    public Environment env;
    
    //****************************************************
    // Internal state vartiables
    //****************************************************
    public List<Token> tokens;
    /**
     * line begin offsets for fast jumps
     */
    public List<Integer> bMarks = new ArrayList<>();
    /**
     * line end offsets for fast jumps
     */
    public List<Integer> eMarks = new ArrayList<>();
    /**
     * offsets of the first non-space characters (tabs not expanded)
     */
    public List<Integer> tShift = new ArrayList<>();
    /**
     * indents for each line (tabs expanded)
     */
    public List<Integer> sCount = new ArrayList<>();

    // An amount of virtual spaces (tabs expanded) between beginning
    // of each line (bMarks) and real beginning of that line.
    //
    // It exists only as a hack because blockquotes override bMarks
    // losing information in the process.
    //
    // It's used only when expanding tabs, you can think about it as
    // an initial tab length, e.g. bsCount=21 applied to string `\t123`
    // means first tab should be expanded to 4-21%4 === 3 spaces.
    //
    public List<Integer> bsCount = new ArrayList<>();
    
    //****************************************************
    // block parser variables
    //****************************************************
    /**
     * required block content indent
     * (for example, if we are in list)
     */
    public int blkIndent = 0;

    /**
     * line index in src
     */
    public int line = 0;

    /**
     * lines count
     */
    public int lineMax = 0;

    /**
     * loose/tight mode for lists
     */
    public boolean tight = false;

    /**
     * indent of the current dd block (-1 if there isn't any)
     */
    public int ddIndent = -1;

    /**
     * if `list`, block parser stops on two newlines
     */
    public String parentType = "root";
    
    public int level = 0;
    
    /**
     * renderer
     */
    public String result;
    
    
    public StateBlock(){}
    
    public StateBlock(String src, MarkMeParser md, Environment env, List<Token> tokens)
    {
        char ch;
        String s;
        int start, pos, len, indent, offset;
        boolean indent_found;
        
        this.src = src;
        this.md = md;
        this.env = env;
        this.tokens = tokens;
        
        // Create caches
        // Generate markers.
        s = this.src;
        indent_found = false;
        
        for (start = pos = indent = offset = 0, len = s.length(); pos < len; pos++)
        {
            ch = s.charAt(pos);

            if (!indent_found) {
                if (Utils.isSpace(ch))
                {
                    indent++;
                    if (ch == 0x09)
                        offset += 4 - offset % 4;
                    else
                        offset++;
                    continue;
                }
                else
                    indent_found = true;
            }

//            // \r 0x0D
//            // \n 0x0A
//            if (ch == 0x0D)// || pos == len - 1)
//            {
//                //if (ch != 0x0A)
//                //    pos++;
//
//                char nextChar = s.charAt(++pos);
//                if (nextChar == 0x0A)
//                {
//                    this.bMarks.add(start);
//                    this.eMarks.add(pos);
//                    this.tShift.add(indent);
//                    this.sCount.add(offset);
//                    this.bsCount.add(0);
//
//                    indent_found = false;
//                    indent = 0;
//                    offset = 0;
//                    start = pos + 1;
//                }
//                else
//                {
//                    this.bMarks.add(start);
//                    this.eMarks.add(pos--);
//                    this.tShift.add(indent);
//                    this.sCount.add(offset);
//                    this.bsCount.add(0);
//
//                    indent_found = false;
//                    indent = 0;
//                    offset = 0;
//                    start = pos + 1;
//                }
//            }
//
//            else if (ch == 0x0A)
//            {
//                char nextChar = s.charAt(++pos);
//                if (nextChar == 0x0D)
//                {
//                    this.bMarks.add(start);
//                    this.eMarks.add(pos);
//                    this.tShift.add(indent);
//                    this.sCount.add(offset);
//                    this.bsCount.add(0);
//
//                    indent_found = false;
//                    indent = 0;
//                    offset = 0;
//                    start = pos + 1;
//                }
//                else
//                {
//                    this.bMarks.add(start);
//                    this.eMarks.add(pos--);
//                    this.tShift.add(indent);
//                    this.sCount.add(offset);
//                    this.bsCount.add(0);
//
//                    indent_found = false;
//                    indent = 0;
//                    offset = 0;
//                    start = pos + 1;
//                }
//            }
//            else if (pos == len - 1)
//            {
//                pos++;
//
//                this.bMarks.add(start);
//                this.eMarks.add(pos);
//                this.tShift.add(indent);
//                this.sCount.add(offset);
//                this.bsCount.add(0);
//
//                indent_found = false;
//                indent = 0;
//                offset = 0;
//                start = pos + 1;
//            }

            if (ch == 0x0A || pos == len - 1)
            {
                if (ch != 0x0A)
                    pos++;

                this.bMarks.add(start);
                this.eMarks.add(pos);
                this.tShift.add(indent);
                this.sCount.add(offset);
                this.bsCount.add(0);

                indent_found = false;
                indent = 0;
                offset = 0;
                start = pos + 1;
            }


        }
        
        // Push fake entry to simplify cache bounds checks
        this.bMarks.add(s.length());
        this.eMarks.add(s.length());
        this.tShift.add(0);
        this.sCount.add(0);
        this.bsCount.add(0);

        // don't count last fake line
        this.lineMax = this.bMarks.size() - 1;
    }
    
    /**
     * Push new token to "stream"
     */
    public Token push(String type, String tag, int nesting)
    {
        Token token = new Token(type, tag, nesting);
        token.block = true;

        if (nesting < 0)
            this.level--;

        token.level = this.level;

        if (nesting > 0)
            this.level++;

        this.tokens.add(token);

        return token;
    }
    
    @Override
    public boolean isEmpty(int line)
    {
        return this.getbMarks(line) + this.gettShift(line) >= this.geteMarks(line);
    }
    
    public int skipEmptyLines(int from)
    {
        for (int max = this.lineMax; from < max; from++)
        {
            if (this.getbMarks(from) + this.gettShift(from) < this.geteMarks(from))
                break;
        }
        return from;
    }
    
    /**
     * Skip spaces from given position
     */
    @Override
    public int skipSpaces(int pos)
    {
        char ch;

        for (int max = this.src.length(); pos < max; pos++)
        {
            ch = this.src.charAt(pos);
            if (!Utils.isSpace(ch))
                break;
        }

        return pos;
    }
    
    /**
     * Skip spaces from given position in reverse
     */
    @Override
    public int skipSpacesBack(int pos, int min)
    {
        if (pos <= min)
            return pos;

        while (pos > min)
        {
            if (!Utils.isSpace(this.src.charAt(--pos)))
                return pos + 1;
        }

        return pos;
    }
    
    /**
     * Skip char codes from given position
     */
    @Override
    public int skipChars(int pos, int code)
    {
        for (int max = this.src.length(); pos < max; pos++)
        {
            if (this.src.charAt(pos) != code)
                break;
        }

        return pos;
    }
    
    /**
     * Skip char codes reverse from given position - 1
     */
    @Override
    public int skipCharsBack(int pos, int code, int min)
    {
        if (pos <= min)
            return pos;

        while (pos > min)
        {
            if (code != this.src.charAt(--pos))
                return pos + 1;
        }

        return pos;
    }
    
    /**
     * cut lines range from source
     */
    @Override
    public String getLines(int begin, int end, int indent, boolean keepLastLF)
    {
        int lineIndent, first, last, lineStart, line = begin;
        char ch;

        if (begin >= end)
            return "";

        String[] queue = new String[end - begin];
        
        for (int i = 0; line < end; line++, i++)
        {
            lineIndent = 0;
            lineStart = first = this.getbMarks(line);

            if (line + 1 < end || keepLastLF)
            {
                // No need for bounds check because we have fake entry on tail.
                last = this.geteMarks(line) + 1;
            }
            else
            {
                last = this.geteMarks(line);
            }

            while (first < last && lineIndent < indent)
            {
                ch = this.src.charAt(first);

                if (Utils.isSpace(ch))
                {
                    if (ch == 0x09)
                        lineIndent += 4 - (lineIndent + this.getBsCount(line)) % 4;
                    else
                        lineIndent++;
                }
                else if (first - lineStart < this.gettShift(line))
                {
                    // patched tShift masked characters to look like spaces (blockquotes, list markers)
                    lineIndent++;
                }
                else
                {
                    break;
                }

                first++;
            }

            if (lineIndent > indent)
            {
                // partially expanding tabs in code blocks, e.g '\t\tfoobar'
                // with indent=2 becomes '  \tfoobar'
                queue[i] = String.join(" ", new String[lineIndent - indent + 1]) + StringUtil.slice(this.src, first, last);
            }
            else
            {
                queue[i] = StringUtil.slice(this.src, first, last);
            }
        }
        
        return String.join("", queue);
    }

    @Override
    public Environment getEnv()
    {
        return env;
    }

    @Override
    public void setEnv(Environment env)
    {
        this.env = env;
    }

    @Override
    public String getSrc()
    {
        return src;
    }

    public void setSrc(String src)
    {
        this.src = src;
    }

    @Override
    public MarkMeParser getMd()
    {
        return md;
    }

    public void setMd(MarkMeParser md)
    {
        this.md = md;
    }

    @Override
    public List<Token> getTokens()
    {
        return tokens;
    }

    @Override
    public Token getToken(int index)
    {
        return tokens.get(index);
    }

    @Override
    public void setTokens(List<Token> tokens)
    {
        this.tokens = tokens;
    }

    @Override
    public void setToken(int index, Token token)
    {
        tokens.set(index, token);
    }

    @Override
    public List<Integer> getbMarks()
    {
        return bMarks;
    }

    @Override
    public int getbMarks(int index)
    {
        return bMarks.get(index);
    }

    @Override
    public void setbMarks(List<Integer> bMarks)
    {
        this.bMarks = bMarks;
    }

    @Override
    public void setbMarks(int index, int value)
    {
        bMarks.set(index, value);
    }

    @Override
    public List<Integer> geteMarks()
    {
        return eMarks;
    }

    @Override
    public int geteMarks(int index)
    {
        return eMarks.get(index);
    }

    @Override
    public void seteMarks(List<Integer> eMarks)
    {
        this.eMarks = eMarks;
    }

    @Override
    public List<Integer> gettShift()
    {
        return tShift;
    }

    @Override
    public int gettShift(int index)
    {
        return tShift.get(index);
    }

    @Override
    public void settShift(List<Integer> tShift)
    {
        this.tShift = tShift;
    }

    @Override
    public void settShift(int index, int value)
    {
        tShift.set(index, value);
    }

    @Override
    public List<Integer> getsCount()
    {
        return sCount;
    }

    @Override
    public int getsCount(int index)
    {
        return sCount.get(index);
    }

    @Override
    public void setsCount(List<Integer> sCount)
    {
        this.sCount = sCount;
    }

    @Override
    public void setsCount(int index, int value)
    {
        sCount.set(index, value);
    }

    @Override
    public List<Integer> getBsCount()
    {
        return bsCount;
    }

    @Override
    public int getBsCount(int index)
    {
        return bsCount.get(index);
    }

    @Override
    public void setBsCount(List<Integer> bsCount)
    {
        this.bsCount = bsCount;
    }

    @Override
    public void setBsCount(int index, int value)
    {
        bsCount.set(index, value);
    }

    @Override
    public int getBlkIndent()
    {
        return blkIndent;
    }

    @Override
    public void setBlkIndent(int blkIndent)
    {
        this.blkIndent = blkIndent;
    }

    @Override
    public int getLine()
    {
        return line;
    }

    @Override
    public int getLineMax()
    {
        return lineMax;
    }

    @Override
    public void setLineMax(int lineMax)
    {
        this.lineMax = lineMax;
    }

    @Override
    public boolean isTight()
    {
        return tight;
    }

    @Override
    public void setTight(boolean tight)
    {
        this.tight = tight;
    }

    @Override
    public void setLine(int line)
    {
        this.line = line;
    }

    @Override
    public int getLevel()
    {
        return level;
    }

    @Override
    public void setLevel(int level)
    {
        this.level = level;
    }

    @Override
    public String getParentType()
    {
        return parentType;
    }

    @Override
    public void setParentType(String parentType)
    {
        this.parentType = parentType;
    }
}
